Skip to content

obs(cloud): dedupe redactEmail, classify mailer + startup logs - #364

Merged
oratis merged 1 commit into
mainfrom
claude/obs-followups-logging
Aug 21, 2026
Merged

obs(cloud): dedupe redactEmail, classify mailer + startup logs#364
oratis merged 1 commit into
mainfrom
claude/obs-followups-logging

Conversation

@oratis

@oratis oratis commented Aug 20, 2026

Copy link
Copy Markdown
Owner

#363 的三个收尾项——都是在生产验收那个 PR 时查出来的(见下方「怎么发现的」)。纯日志卫生,无行为变更。

1. redactEmail 有两份实现,且行为不一致

位置 alice.smith@example.com trailing@
旧 A src/log.ts#363 新加) a***@example.com a***@泄漏原文
旧 B src/web/mailer.ts:74(早已存在) al***@example.com ***

是我在 #363 里没查重引入的重复。保留更严谨的 B(lastIndexOf + 处理 @ 结尾),把它移进 log.ts 与其它脱敏函数放一起——依赖方向因此是 web/mailer → log 而不是反过来;mailer.ts 重新导出,其既有调用方和 mailer.test.ts 的 5 个 case 不受影响。

2. mailer.ts 四处 console.error 未分级

发信成功和发信失败此前记在同一级别。现在:outcome=success → INFO,skipped_no_key → WARNING,两条 send_failed → ERROR。收件人本来就是脱敏的(redactEmail(to)),这点没动。

3. 启动路径完全没有 severity

cli.ts 两行 serve 日志、soul/git.ts 的 git-missing 警告、entrypoint.sh 三处 echo。前两者走 logger;shell 那三处加了个 log_line helper,逻辑与 src/log.ts 一致——K_SERVICE 下输出单行 JSON,否则纯文本(消息是固定串、无引号无反斜杠,不需要 JSON 转义)。

怎么发现的

在生产核实 #363 时按 revision 分组读日志,发现新旧代码的分界线是 00020reviewer@meetlisa.air***@meetlisa.ai (em-2…b215))。当前 00021 承 100% 流量、[sweep]/[orchestrator] 已是 INFO——但同一批日志里仍有 4 条 severity 为空,顺藤摸到上面这些。

验证

  • npm test 1637 pass / 0 fail;typecheckcheck:api-contract 均干净。
  • log.test.ts 的 redactEmail 用例已按更严格的契约更新(含 trailing@ 这个原先会泄漏的 case)。
  • sh -n deploy/entrypoint.sh 通过;log_line 两种模式实测:K_SERVICE 下输出 {"severity":"INFO","message":"[cloud] soul already present — skipping birth"},无 K_SERVICE 时输出原纯文本。
  • 本地模式下 logger 仍 fallback 到 console.error,所以 mailer.test.ts 里捕获日志的既有测试无需改动。

不在本 PR 范围

  • src/billing/meter.ts:114 仍是裸 console.error(anomaly 告警因此要匹配 textPayload,云端已用双写法兜住)。
  • meter.ts 的 anomaly 去重仍是进程内存态(MAX_INSTANCES>1 会重复告警,需共享态)。
  • src/cli/* 的 console 调用是终端给用户看的输出,不应改成结构化日志。

🤖 Generated with Claude Code

Follow-ups to #363, found while verifying that PR in production.

- redactEmail existed twice with different behavior: src/log.ts kept 1
  char via indexOf, src/web/mailer.ts:74 kept 2 via lastIndexOf and also
  handled a trailing "@". The stricter one is now the only one — it moves
  into log.ts beside the other redactors (so the dependency runs web →
  log, not the reverse) and mailer.ts re-exports it for its own callers.
- mailer.ts's four log sites were still bare console.error, so a
  successful send filed at the same level as a failed one: outcome=success
  is INFO, skipped_no_key is WARNING, both send_failed paths are ERROR.
  Recipients were already redacted; that is unchanged.
- Startup path had no severity at all on Cloud Run — cli.ts's two serve
  lines, soul/git.ts's git-missing warning, and entrypoint.sh's three
  echoes. The shell ones get a log_line helper mirroring src/log.ts:
  JSON under K_SERVICE, plain text otherwise.

Verified: /health-serving revision 00021 shows [sweep]/[orchestrator] at
INFO and reviewer email redacted, so this only covers what #363 missed.

npm test 1637 pass / 0 fail; typecheck + api-contract clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oratis
oratis merged commit a4a1e39 into main Aug 21, 2026
1 check passed
oratis added a commit that referenced this pull request Aug 21, 2026
The >$10/day/user anomaly alert deduped through a process-local Set, so
with MAX_INSTANCES>1 the same day paged the operator once per instance,
and a cold start re-armed it. The Set stays as the in-process fast path;
the cross-instance arbiter is now a create-only Firestore write on
lisa-anomaly-alerts/{uid}_{day} — the second writer fails its
precondition, and that failure is the dedup signal.

Deliberately fail-open: only 409/412 counts as "someone else has it".
Any other failure (network, permission, Firestore down) still alerts,
because a duplicate alert is a nuisance while a missed one is an
unnoticed burn. With Firestore off or outside a per-uid scope (Mac
edition, shared-token demo) the claim is a no-op and behavior is
unchanged — verified by two tests that assert the network is never hit.

Also finishes this file's share of #363/#364: both console.error sites
become logError, and the alert text names a cloud tenant by redacted uid
instead of embedding the home path (which contains the raw uid). The
"[billing] ⚠ anomaly" prefix the alert policy matches is untouched, and
that policy already matches both textPayload and jsonPayload, so the
move to structured output does not break it.

npm test 1643 pass / 0 fail; typecheck + api-contract clean.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant